﻿using System;
using System.Collections.Generic;
using System.Threading;

namespace AzureDriveCore
{
	public class RequestCacheManager
	{
		private Dictionary<string, CacheResult> _cache = new Dictionary<string, CacheResult>();

		public void ClearCache(RequestType type, string fileName)
		{
			string key = "" + type + (fileName == null ? "#null" : "#" + fileName);

			lock (_cache)
			{
				if (_cache.ContainsKey(key))
				{
					_cache.Remove(key);
				}
			}
		}

		public T ExecuteOrGetFromCache<T>(int duration, Func<T> task, RequestType type, string fileName)
		{
			return this.ExecuteOrGetFromCache(duration, null, task, type, fileName);
		}

		public T ExecuteOrGetFromCache<T>(int duration, int? recycleDuration, Func<T> task, RequestType type, string fileName)
		{
			string key = "" + type + (fileName == null ? "#null" : "#" + fileName);

			CacheResult result;

			lock (_cache)
			{
				if (_cache.ContainsKey(key) == false)
				{
					_cache.Add(key, new CacheResult());
				}

				result = _cache[key];
			}

			lock (result)
			{
				// Not cached, simple case scenario - go get it!
				if (result.CacheTime == null)
				{
					result.Result = task();

					result.CacheTime = DateTime.UtcNow;

					return (T)result.Result;
				}
			}

			// Ok, we are cached, but is the cache stale?
			if (result.CacheTime < DateTime.UtcNow.AddSeconds(-1 * duration))
			{
				// Cache is stale, recycle (but be weary that another thread may already be doing this job).
				lock (result)
				{
					// We have to re-ask the question since we could have come behind another thread.
					if (result.CacheTime < DateTime.UtcNow.AddSeconds(-1 * duration))
					{
						result.Result = task();

						result.CacheTime = DateTime.UtcNow;

						return (T)result.Result;
					}
					else
					{
						return (T)result.Result;
					}
				}
			}
			else
			{
				// Is background recycling not being used? Simple scenario, just return the cache.
				if (recycleDuration == null)
				{
					return (T)result.Result;
				}
				else
				{
					// We are cached, the cache is not stale, but is it getting close to being stale? If it is
					// not, then this is also a simple scenario - just return the cache.
					if (result.CacheTime >= DateTime.UtcNow.AddSeconds(-1 * recycleDuration.Value))
					{
						return (T)result.Result;
					}
					else
					{
						// Queue up a background thread to begin the fetch. Do this with dancing lock objects
						// (or "swinging blades" as I like to call them).
						if (result.IsRecycling)
						{
							return (T)result.Result;
						}
						else
						{
							// Thechnically, multiple threads could be queued here in the rare chance that two
							// threads hit that "if" statement above, came here, and one set it to true. We'll
							// handle that in the background thread below.
							lock (result.RecycleSyncLock)
							{
								result.IsRecycling = true;

								// Hitting this if statement means that two or more threads came in, the first
								// re-ran the cache, and therefore this second thread should never have come
								// here, so we will simply return.
								if (result.CacheTime >= DateTime.UtcNow.AddSeconds(-1 * recycleDuration.Value))
								{
									result.IsRecycling = false;

									return (T)result.Result;
								}

								new Thread(new ThreadStart(delegate
								{
									lock (result.RecycleSyncLock)
									{
										result.Result = task();

										result.CacheTime = DateTime.UtcNow;

										result.IsRecycling = false;
									}
								})) { IsBackground = true }.Start();

								// Return the perfectly valid cached object, now that we have kicked off the
								// process to refreshen it.
								return (T)result.Result;
							}
						}
					}
				}
			}
		}
	}

	public enum RequestType
	{
		CreateFile,
		FindFiles,
		OpenDirectory,
		GetFileInformation,
	}

	class CacheResult
	{
		public DateTime? CacheTime { get; set; }

		public bool IsRecycling;

		public object RecycleSyncLock = new object();

		public object Result { get; set; }
	}
}
